Repository navigation
Migrate to Vite 8 / ESM TypeScript, add test suite, SSR-safe manual-activation ESM build (v2.0.0) - #27
Open
yellowby wants to merge 81 commits into
Open
Migrate to Vite 8 / ESM TypeScript, add test suite, SSR-safe manual-activation ESM build (v2.0.0)#27yellowby wants to merge 81 commits into
yellowby wants to merge 81 commits into
Conversation
- Replace webpack 5 + ejs-loader with Vite 8 (Rolldown) driven from scripts/build.js: five single-entry IIFE builds emit the classic unhashed bundles (embed.js, embed-options.js, options.js, options.i18n.de/fr.js + .min.js variants), plus a multi-entry ESM build in dist/esm/ and tsc-generated declarations in dist/types/. - Rewrite all src/ as strict-TypeScript ES modules. Side-effectful modules now export boot() called by the entries in the original CJS execution order under the _loaded guard, so double-loading bundles still boots exactly once despite ESM import hoisting. - Replace the six EJS templates with template-literal functions in options/renderer.ts. Fixes the group template, which compiled to _.escape() without lodash bundled and threw at runtime, and adds HTML-escaping of scalar interpolations (previously raw). - Drop legacy-browser support: ActiveX/Flash detection, attachEvent, vendor-prefixed rAF/setImmediate, IE styleSheet.cssText, innerText fallbacks, and the Chrome-77 UA sniff (lazy-loading-native.js removed; SUPPORT_IFRAME_LOADING_ATTR hardcoded true). Build targets Vite's baseline-widely-available. - package.json: type=module, v2.0.0, exports map (., ./options, ./i18n/de, ./i18n/fr) pointing at dist/esm + dist/types; webpack and ejs devDependencies removed; flat ESLint config with typescript-eslint; delete stale dist/embed-autoplay*. - Add demo/ smoke-test pages used to verify script-src config parsing, API surface parity, options form + i18n, double-load, and ESM consumption in a real browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Unit tests (tests/unit/, jsdom): utils (query-string handling, endpoint building, extendOptions normalization, widget wrappers, setStyles), events pub/sub, messaging (postMessage origin trimming, findIframe incl. shadow roots, message listener + pym fallback), theme, options renderer (escaping semantics, all six templates), form-generator, form-builder, and translator. 72 tests. - Browser tests (tests/e2e/, Playwright + chromium): script-src config parsing, anchor unfurl to the exact API endpoint, resize and cancelWidget message flows, link recovery on cancel, pre-migration API surface snapshot, options form + German i18n second-script flow, double-load boots once, ESM build singleton identity. The Iframely endpoint is route-stubbed, so tests need no network or API key. - demo/ pages double as the e2e fixtures and a manual playground served by the new scripts/serve.mjs (pnpm serve); Playwright starts it automatically. Replace stale demo/old.html with demo/cancel-recover.html. - Fix Iframely.setTheme type to include its optional container param. - Scripts: test, test:watch, test:e2e, serve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Demo pages use an {{IFRAMELY_API_KEY}} placeholder in script srcs;
scripts/serve.mjs substitutes it at request time from the
environment or a git-ignored .env at the repo root, falling back to
the inert TEST placeholder. The key is only injected into locally
served responses and never written to committed files.
- Add .env.example; git-ignore .env.
- Pin the Playwright web server to IFRAMELY_API_KEY=TEST and disable
reuseExistingServer so tests never assert against a dev server that
is running with a real key.
- Document the workflow in the README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TypeScript 5.9 -> 7.0 (native compiler) for typecheck and declaration emit; tsgo requires an explicit rootDir in tsconfig.build.json for the dist/types layout. The typescript-eslint dependency was what pinned TS to 5.x. - Replace ESLint (+ @eslint/js, globals, typescript-eslint) with oxlint: correctness category as errors plus the carried-over no-console/no-empty rules, configured in oxlint.config.ts. - Add oxfmt for formatting, configured in oxfmt.config.ts: tabs (width 4), single quotes, no trailing commas; dist/, demo/ and markdown excluded. One-time reformat of the codebase to tabs. - Scripts: lint -> oxlint, new format/format:check; prebuild now runs typecheck + lint + format:check. - Pin Node 24.18 via .node-version: the *.config.ts files rely on Node native type stripping, which the previous default Node 22.16 lacks. No engines field so npm consumers are unaffected. - Rewrite the events test this-binding assertion via vi.fn mock contexts (oxlint no-this-alias). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ESM entries (main.ts, main-options.ts) no longer auto-run on
DOMContentLoaded: config.autorun defaults to false, and consumers
activate embeds explicitly with iframely.load(). Pre-set
window.iframely = { config: { autorun: true } } before importing to
restore the script-tag behavior. IIFE script bundles are unchanged.
- Importing is now safe in isomorphic environments (SvelteKit, etc.):
on the server iframely.ts exports a detached, stateless no-op stub;
dom-ready and the entry bootstraps skip without browser globals, so
a server pass neither throws nor runs side effects.
- Rename extendOptions to iframely.configure; extendOptions stays as
a deprecated alias registered in deprecated.ts and marked
@deprecated in the types. Internal callers switched.
- Tests: new node-environment SSR suite (imports every public entry,
asserts inert no-op behavior), new e2e spec proving no auto-run on
import and activation via load(), surface snapshot gains the
`configure` key, alias identity pinned in unit tests.
- Document the ESM/isomorphic usage flow in the README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- unload(container?) removes the widgets the library built (domain- matched iframes and pending data-iframely-url lazy iframes with the proper wrapper structure; foreign iframes untouched), firing unload-widget per widget and a final unload event. - Modules release their state through those events: intersection unobserves removed iframes and drops all observers on a full unload (observers held strong refs - the actual leak in long-lived SPAs), lazy-img-placeholder clears pending waiting-widget timers, import resets transient import state. - Not covered by design: restoring consumed anchors and import/shadow widgets; documented in types and README. SSR stub gains a no-op. - Tests: unload unit suite, e2e load->unload round trip, surface snapshot + SSR no-op coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildOptionsForm now checks at call time that the embed bundle has provided iframely.trigger and iframely.addEventListener; without them it logs a console.warn and returns instead of throwing from form-builder. Checked per call, not at boot, so loading options.js before embed.js keeps working. Covered by a unit test booting only the options module and an e2e spec with an options.js-only fixture page asserting zero page errors, an empty container, and the warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
configure() refuses to override a key whose current value is false so
that script-src params can permanently disable a feature. The ESM
entries write autorun = false at import time, which turned
iframely.configure({ autorun: true }) into a silent no-op and left
pre-setting window.iframely before the import as the only way back to
script-tag behavior. autorun is a mode, not a feature flag: exempt it
from the sticky-false rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The exports map declared only "types" and "import", so a CommonJS require() on Node 22.12+/24 (which supports require(esm)) and any resolver matching on the "default" condition failed with ERR_PACKAGE_PATH_NOT_EXPORTED. Point "default" at the same ESM file for all four entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The oxlint/oxfmt TypeScript config files rely on Node's native type stripping, so older Node fails late in prebuild with a parser error and no hint of the actual requirement. devEngines (rather than engines) makes pnpm check this up front for contributors without warning every consumer who installs the browser library on an older Node. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Vite migration replaced the old Chrome >= 77 check with a constant true, so browsers without native iframe lazy loading (Safari < 16.4, Firefox < 121) got loading="lazy" ignored and, because the flag said native support was present, lost the server-side lazy=1 fallback as well. Detect the attribute on the iframe prototype at boot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server stub provided only no-op methods, while the Iframely type declares VERSION, DOMAINS, CDN, SUPPORTED_THEMES and the pre-2.0 widgets/events namespaces as always present. Isomorphic code reading any of them during SSR threw. Move the constants into a createConstants() factory shared by the browser boot (const.ts) and the stub; a factory so that the stub, one module instance shared across requests, never aliases arrays a page could mutate. The boot keeps preserving a CDN set before load, as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
index-options.ts ran the options boot inside the same `_loaded` guard that protects the core bootstrap, so whenever a plain embed entry had evaluated first (embed.js followed by embed-options.js, or the base npm entry imported before the options entry) the whole block was skipped and iframely.buildOptionsForm never existed. Move the core bootstrap into bootCore(), shared by both entries and guarded once, and boot options on every options entry, as the standalone options.js bundle already did. The double-load browser test only counted 'init' and therefore passed on the broken behavior; it now also asserts buildOptionsForm exists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
exec_body_scripts walked the live childNodes list while removing the <script> nodes it collected, so the sibling that shifted into the freed index was skipped: it stayed in the DOM inert (innerHTML-inserted scripts never execute) and was never recursed into. A widget whose HTML carried two adjacent scripts lost the second one. Iterate over a snapshot. Adds the first unit tests for the import flow; the JSONP request is intercepted before it reaches the document so nothing is fetched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
unload() cleared the import cache but left the in-flight JSONP <script> in <head>. When it responded (or errored), importReady() re-triggered 'load' on the anchors still in the page and rebuilt widgets in a view that had just been torn down. Now a full unload removes the pending script and its error handler, buildImportWidgets ignores a response for an import that is no longer pending (a removed <script> may still execute), and the anchors queued for that import lose data-import-uri so a later load() considers them again instead of skipping them forever. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When an option's value matched none of its choices, no radio was checked and the radio getter dereferenced a missing input with a non-null assertion. Any later change elsewhere in the form threw inside the change handler, so 'options-changed' never fired again. Report an unselected group as "no value"; the query builder already drops undefined keys. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
messaging.ts passes an undefined widget when the posting iframe has no .iframely-embed/.iframely-responsive wrappers, a layout the resize path supports. The intersection handler dereferenced widget.iframe without the guard its sibling handlers use and threw out of the window message listener. Adds the first intersection tests, with a stubbed observer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With embed.js?parent=<class>, cancelWidget removes the host's wrapper around the library's own wrappers, but unload() only removed the max-width wrapper and left an empty host element in the layout. Share cancelWidget's ancestor lookup as utils.findParentByClass and use it in both places. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The key of each option, which comes straight from the API response, was concatenated into '[name="<key>"]'. A key containing a quote or a backslash made querySelectorAll throw a SyntaxError while binding the form, so the form rendered without any change handlers. The templates already HTML-escape the same key; escape it for the selector too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The slider template interpolates the option value into style="--val: ...", and HTML escaping does not neutralize CSS there: a string value such as "50; background:url(...)" from a setIframelyEmbedOptions message became a live declaration. The generator accepted any string for a ranged option. Coerce the value to a number and fall back to the range minimum when it is not one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The containment check compared the resolved path with startsWith(root) and no trailing separator, so a request such as /..%2f<sibling>/file (URL parsing neutralizes a plain /../, an encoded slash survives it) resolved to a sibling directory of the repo and was served. Compare against the root as a directory prefix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lazy-img-placeholder.ts carried a byte-for-byte copy of the private helper in utils.ts, so the rule for what counts as a wrapper child could silently diverge between getIframeWrapper and hidePlaceholderThumbnail. Export the utils version and use it in both places. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The emitted declarations copy import specifiers verbatim, so the extensionless relative imports in src/ made every shipped .d.ts fail to resolve under moduleResolution node16/nodenext (TS2834), which also hid the re-exported types from such consumers. Explicit .js specifiers are mapped back to the .ts sources by TypeScript, Vite and Vitest, and emit declarations that resolve everywhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nothing exercised the package as npm consumers see it: the browser tests load dist/esm by path and the unit tests import src/, so the exports map and the shipped declarations were unverified (the missing "default" condition would never have surfaced, nor the nodenext declaration failure fixed in the previous commit). scripts/check-package.mjs packs the tarball, installs it into a throwaway consumer directory, imports and requires every "exports" entry, asserts the server stub, and typechecks a strict consumer under both bundler and nodenext resolution with skipLibCheck off. Run with `pnpm test:pkg`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The containment rule lived inline in the request handler, so the fix for the sibling-directory bypass had no automated test (a live test would need a sibling directory sharing the repo's name). Move it into scripts/serve-path.mjs as resolveUnderRoot() and cover the root itself, paths below it, decoded-slash traversal to a same-prefix sibling, and decoded-backslash traversal on Windows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
widgetsCache kept the anchors queued for an import whenever the cycle ended without a response for their url (request error, or a response that did not cover them). A later import of the same url then rendered into the stale, detached elements as well, firing the widget events once per leftover entry. importReady() now empties the cache along with the data-import-uri markers it already clears. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On pushes to main and dev and on pull requests. One job runs the frozen install (with pnpm's supply-chain checks), pnpm audit, typecheck, lint, format check, unit tests, the build, a check that the committed dist/ matches the build, and the package check. A second job runs the Playwright tests in Chromium, Firefox and WebKit and uploads the test results on failure. Actions are pinned to commit SHAs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adopted from publicly's audit.yml: pnpm audit is compared against the committed audit.log snapshot on every push, on PRs to dev and main, weekly on Mondays and on demand. A new advisory fails the run until the snapshot is reviewed and refreshed with `pnpm audit > audit.log`. Actions are SHA-pinned like ci.yml; Node comes from .node-version. CI no longer runs a plain pnpm audit, which would have failed on advisories already acknowledged in the snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iframely.VERSION is the API protocol version sent as v= and must stay 1, so nothing told which build was running. LIB_VERSION is injected from package.json at build time (and in Vitest) via a define, and is also on the server stub. Unbuilt sources fall back to a dev placeholder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If a pre-2.0 embed.js (typically the cloud script API embed codes append) booted first, bootCore() returned silently and the page later failed with "iframely.unload is not a function". Taking over the foreign core is not possible (its listeners and widgets live in its own closure), so warn once, naming both copies and how to load just one. Same-major sharing stays silent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
USAGE documented `cdn` as a configure() key, but nothing read config.cdn, so iframes served from your own CDN were neither resized nor found by unload(). configure() now normalises the value (protocol and trailing slash stripped, disabled values ignored) into iframely.CDN and keeps it out of config. A pre-set config.cdn applies at boot, and a script src ?cdn= still wins, now through the same path. Message routing uses knownDomains(), so a CDN assigned with a protocol matches too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
load() only knew the whole document, an <a>, a lazy <iframe> or (container, url); any other element was ignored, so components had to rescan the page on every mount while unload(container) was already scoped. A shared loadRoot() helper now lets the document-scan handlers take an element, fragment or shadow root: links, lazy iframes, size requests to existing iframes, and import-mode batching of the container's links. A document-wide load() now also requests sizes, as USAGE already promised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two behaviours integrations rely on were undocumented: unload(el) works on a container the framework already detached, and since unload() does not restore consumed anchors, each new API response needs fresh markup. USAGE gets a "Framework lifecycle" section, and the Svelte (now an attachment) and React (now keyed on html) recipes pair load(el) with unload(el). A test locks in the detached case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
on/trigger/triggerAsync, load() and configure() took any arguments, so consumers got no help with signatures. An IframelyEvents map (payloads checked against every trigger site) types the callbacks by event name; the fallback overload only accepts names outside the map, so a known event with a wrong payload is rejected instead of falling through to any. load() gets one overload per form, configure() takes IframelyConfig. The import loader's internal 'load' re-dispatch with widget data goes through an untyped view. check-package adds typed consumer calls and @ts-expect-error cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The snippet called iframely.configure() right after an async embed.js
tag. The script has not run at that point, so it threw "iframely.configure
is not a function" in every browser. Pre-setting window.iframely = {
config } before the tag is read at boot regardless of load timing, with
the src query string merged on top. A demo page and an e2e test cover
the pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VERSION read like the library release but is the API protocol version sent as v=. Next to LIB_VERSION the new name says what it is. The library now reads API_VERSION only; VERSION stays on the surface with the same value as a deprecated alias for existing integrations, as extendOptions did for configure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The warning compared major versions, so an older 2.x core missing a method a newer 2.x copy provides stayed silent and the page failed with "... is not a function". It now checks the booted core for every method this copy installs (CORE_API, kept in sync with the boot by a test) and names the missing ones; a different major still warns, since the same names may behave differently. LIB_VERSION names both copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LIB_VERSION only helps once the script has run. A copy pasted into a CMS or fetched from the cloud CDN had no name or version at all. All 17 bundles (minified and ESM included) now start with /*! @iframely/embed.js <version> | MIT | <repo> */, which also carries the license notice. check-package verifies it on the packed tarball, and the README tells reporters where to find the version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package is @iframely/embed.js; @iframely/embedjs does not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Version 2.0.0-next.2, for the `next` dist-tag so `latest` stays on 1.x. The Unreleased changelog section is dated. 2.0.0-next.1 was never published to npm, so its section no longer says it was, and next.2 is marked as the first 2.0 release there. dist/ is rebuilt for the new LIB_VERSION and bundle banners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The init handler applied a pre-set window.iframely.config.cdn and then read the script src, where a script at the root of its host (https://static.example.com/embed.js) makes that host the CDN. The fallback overwrote the configured CDN, so endpoints went to the script host and the real CDN's iframes were no longer recognised. The script host now only applies when no CDN was configured; ?cdn= in the src still wins. The e2e fixtures serve from /dist/, which CDN_RE does not match, so only a unit test covers it.
With two or more anchors that can be neither imported nor unfurled (empty data-iframely-url and no href), makeImportAPICall queued nothing and triggered 'load' again, whose import handler found the same anchors: unbounded recursion ending in "Maximum call stack size exceeded", which also kept the later loaders from running. The re-trigger was redundant: every anchor has already gone through the usual build, and the 'load' that started the import pass carries on with the remaining handlers. The importReady() call next to it had nothing to clean up either, since no import was started.
When an import ended (failure, a response that omits a url, or a cancelling unload), the anchors still waiting were found with document.querySelectorAll. Anchors queued by load(shadowRoot), a fragment or a detached container are out of its reach: they kept data-import-uri for good, and both the ahref and the import loader skip such anchors on every later load. They are now taken from widgetsCache, which holds every queued anchor; the document query stays as a supplement for anchors that carry the attribute without being cached.
While an import is pending, both the import and the ahref loader leave a document-wide load alone. That was fine when only a document load could start an import, which then covered every anchor. Since load(container) can start one too, a load() arriving meanwhile (the DOMContentLoaded autorun, typically) was dropped, and the anchors outside the container stayed bare links. The import loader now remembers such a load and replays it when the container import ends. unload(container) also cancels a pending import that the load of that container (or of one inside it) started, so a route change no longer leaves the block in place. The container's anchors are released after the replay, so the waiting document load does not build them.
With the parent option set, unload() removes the nearest ancestor carrying that class together with the widget. The search was not bounded, so for a component whose own root (or an element above it) has the class, unload(el) removed el or its ancestor, with unrelated content, and the framework then failed removing a node already gone. unload(container) now takes a parent wrapper only inside the container and otherwise removes just the widget; a document-wide unload() is unchanged.
Any script or extension can post to the window. A message such as
{ type: 'x', context: { id: 1 } } passed the listener's guard and
reached findIframe, where src.replace threw an uncaught TypeError on
every such message. Only a string is used for the src match now, both
in the listener and in findIframe itself; the iframe is still found by
its contentWindow.
A full unload disconnected and dropped every intersection observer. unload() does not reach into shadow roots, so widgets there stayed on the page but never received another intersection message. Each observer now tracks its targets: unload-widget releases a removed iframe as before, and a full unload releases the iframes that are no longer in the document and drops only the observers left with nothing to observe.
main.ts and main-options.ts wrote autorun = false into window.iframely.config, which every embed.js on the page shares. A script-tag copy (one a CMS adds, or the 1.x cloud script that API embed codes include) then stopped auto-running, and a 1.x copy could not be switched back on: it has no configure(), and its extendOptions does not overwrite false. That write also happened when this copy did not even boot. The default is now per copy, in dom-ready.ts: the npm entries switch their own off, and an explicit config.autorun still applies to every copy. With nothing writing false behind the user's back, configure() no longer needs its special case for the key.
In development StrictMode (the Next.js default) runs an effect, its cleanup and the effect again on the same element. The recipe's first load(el) consumed the anchors, unload(el) removed the widgets, and the second load(el) found nothing, because React does not re-apply an unchanged dangerouslySetInnerHTML: every embed stayed empty. The cleanup now puts the markup back after unload(el). A unit test covers the load, unload, restore, load sequence the recipe relies on.
The dotfile refusal looked for a leading dot only. Windows opens .env and .git under other names too: an 8.3 short name (ENV~1, GIT~1), a trailing dot or space, or an alternate data stream. Path segments of those shapes are refused now. The server binds to loopback, but a page that points its own hostname at 127.0.0.1 (DNS rebinding) could still read the demo pages, which carry the API key. Requests whose Host is not a loopback name get 403.
Version 2.0.0-next.3, for the `next` dist-tag so `latest` stays on 1.x. The Unreleased changelog section is dated. dist/ is rebuilt for the new LIB_VERSION and bundle banners.
Listeners could only be added, so a component that subscribed on mount leaked its callback on every re-mount and had to fan out from one permanent listener instead. on() now returns a function that removes that one registration, and off(event, cb) removes every registration of cb. A removed listener is never called again: dispatch checks a per-listener flag at call time, so that holds for a triggerAsync already waiting for its frame and for a listener removed by an earlier one. The listener arrays are replaced rather than mutated, so removing during a dispatch no longer makes the next listener skip. off joins CORE_API, so the foreign-core warning names it when an older core owns the page. The server stub's on() returns a no-op.
Version 2.0.0-next.4, for the `next` dist-tag so `latest` stays on 1.x. The Unreleased changelog section is dated. dist/ is rebuilt for the new LIB_VERSION and bundle banners.
A page that reserves space for an embed had no reliable signal that the embed has its real size. heightChanged fires only when the computed height changes, and a frame later, so an embed that loads at its final size (a fixed-aspect player) never signals; the alternative was reading the message protocol. iframe-sized fires synchronously from widgetResize for every size an embed reports, once the wrapper styles are set, with the iframe and the reported media. heightChanged was the only camelCase event name. It is now height-changed; the old name is deprecated and still fires. Both are queued from widgetResize, the old one second, so its listeners run after those of the new name.
Version 2.0.0-next.5, for the `next` dist-tag so `latest` stays on 1.x. The Unreleased changelog section is dated. dist/ is rebuilt for the new LIB_VERSION and bundle banners.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Modernizes embed.js end to end: webpack 5 + EJS is replaced with Vite 8 and strict ESM TypeScript, a full unit + browser test suite is added, and the new ESM build becomes manual-activation and SSR-safe with a richer public API (
configure,load/unload). Classic IIFE script bundles keep their filenames and behavior. Version bumps to 2.0.0.Changes by commit
Build & toolchain
scripts/build.js: five single-entry IIFE builds emit the classic unhashed bundles (embed.js,embed-options.js,options.js,options.i18n.de/fr.js+.min.js), plus an ESM build indist/esm/and declarations indist/types/.src/rewritten as strict-TypeScript ES modules; side-effectful modules exportboot()so double-loading a bundle still boots exactly once._.escape()without lodash and threw at runtime) and adds HTML-escaping of scalar interpolations.attachEvent, vendor-prefixed rAF, IE fallbacks, Chrome-77 UA sniff); targets Vite's baseline-widely-available.package.json:type: module, exports map (.,./options,./i18n/de,./i18n/fr)..node-versionfor native type stripping of*.config.ts; noenginesfield, so npm consumers are unaffected.Tests & demo workflow
demo/pages double as e2e fixtures and a manual playground served byscripts/serve.mjs(pnpm serve).serve.mjssubstitutes{{IFRAMELY_API_KEY}}at request time from the environment or a git-ignored the key is never written to committed filee inertTESTkey.ESM public API
iframely.configure(1f11c44)DOMContentLoaded; consumers calliframely.load()explicitly (window.iframely = { config: { autorun: true } }restores script-tag behavior). IIFE bundles are unchanged.extendOptionsrenamed toiframely.configure, withextendOptionskept as a deprecated alias.iframely.unload()as the inverse ofload()(7196103)unload(container?)removes library-built widgets (foreign iframes untouched), firingunload-widgetper widget and a finalunloadevent; modules release observers and timers through those events — fixing an IntersectionObserver leak in long-lived SPbuildOptionsFormnow warns and returns instead of throwing when the embed bundle isn't present.Breaking changes
type: module) with an exports map; deep imports of old paths won't resolve.iframely.load()or opt in toautorun.extendOptionsis deprecated in favor ofiframely.configure(alias still works).🤖 Generated with Claude Code